﻿#region Disclaimer/Info
///////////////////////////////////////////////////////////////////////////////////////////////////
// MetaCollection
// 
// MetaCollection is a system to provide a standard structure, permissions and metadata service
// for content organization.
//
// This project is hosted at Google Code at http://code.google.com/p/metacollection/
//
// This project is licensed under the BSD license.  See the License.txt file for more information.
///////////////////////////////////////////////////////////////////////////////////////////////////
#endregion

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Xml.Linq;
using System.Globalization;
using System.IO;
using MetaCollection.Framework.Model;
using MetaCollection.ContentService.Http;
using Model = MetaCollection.Framework.Model;
using MetaCollection.Framework;
using System.Web.Caching;
using MetaCollection.ContentService.Configuration;
using System.Configuration;
using System.Xml;
using System.Collections;
using System.Text.RegularExpressions;

namespace MetaCollection.ContentService.WebDav
{
	public static class Properties
	{
		public const string CONTENTSERVICE_LOCK_PROPERTY_NAME = "ContentService.Lock";
		private const string MC_NAMESPACE = "uri://MetaCollection/v2";
		private static IRequestHandler Handler = MetaCollection.Framework.RequestHandlerFactory.CreateRequestHandler();

		#region basic property mapping collections

		/// <summary>
		/// WebDav Basic Properties (simple set of names to use when returning directory listings for propfind)
		/// </summary>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2211:NonConstantFieldsShouldNotBeVisible", Justification = "Static classes, properties can not be constant when their type is not a valuetype")]
		public static IEnumerable<XName> BasicNames = new XName[] { 
			XName.Get("displayname", "DAV:"),
			XName.Get("getcontenttype", "DAV:"),
			XName.Get("getetag", "DAV:"),
			XName.Get("getlastmodified", "DAV:"),
			XName.Get("resourcetype", "DAV:"),
			XName.Get("mimetype", MC_NAMESPACE),
			XName.Get("resourceprovider", MC_NAMESPACE)
		};
		// Id | Version | ParentId | Name | AccessClaims | Specification

		/// <summary>
		/// WebDav Basic Properties
		/// </summary>
		/// <remarks>
		/// These properties are the base set required for RFC4918 compliance
		/// </remarks>
		private static IEnumerable<IResourceProperty> Basic = new IResourceProperty[] { 
			new ResourceProperty(XName.Get("creationdate", "DAV:"), "LastModified", o => ((DateTime)o).ToUniversalTime().ToString("s", CultureInfo.InvariantCulture) + "Z"),
			new ResourceProperty(XName.Get("displayname", "DAV:"), "Name"),
			new ResourceProperty(XName.Get("getcontentlanguage", "DAV:"), "id", o => "en-us"), /* we select on ID so our value goes out for each item */
			new ResourceProperty(XName.Get("getcontentlength", "DAV:"), "id", o => "0"), 
			new ResourceProperty(XName.Get("getcontenttype", "DAV:"),"MimeType", o => o.ToString() == "application/x-folder" ? "application/webdav-collection" : o),
			new ResourceProperty(XName.Get("getetag", "DAV:"), "eTag", s=>s.Key.Equals("ID") || s.Key.Equals("VERSION"), o=>o is Guid ? String.Format(CultureInfo.InvariantCulture, "{0},",o) : o.ToString()),
			new ResourceProperty(XName.Get("getlastmodified", "DAV:"), "LastModified", o => ((DateTime)o).ToUniversalTime().ToString("s", CultureInfo.InvariantCulture) + "Z"),
			new LockProperty(),
			new ResourceProperty(XName.Get("resourcetype", "DAV:"), "MimeType", o => o.ToString() == "application/x-folder" ? new XElement(XName.Get("collection","DAV:")) : null)
		};


		/// <summary>
		/// WebDav Extended Properties (including Microsoft extensions)
		/// </summary>
		/// <remarks>
		/// Extended properties which are returned to Microsoft WebDav clients.
		/// </remarks>
		private static IEnumerable<IResourceProperty> Extended = new IResourceProperty[] { 
			new ResourceProperty(XName.Get("ishidden", "DAV:"), "Id", o => "0"),
			new ResourceProperty(XName.Get("lastaccessed", "DAV:"), "LastModified", o => ((DateTime)o).ToUniversalTime().ToString("r", CultureInfo.InvariantCulture)),
			new ResourceProperty(XName.Get("isroot", "DAV:"), "ParentId", o => ((o.ToString() == "00000000-0000-0000-0000-000000000000") || o == null) ? "true" : "false"),
			new ResourceProperty(XName.Get("isfolder", "DAV:"), "MimeType", o => o.ToString() == "application/x-folder" ? "t" : "f"),
			new ResourceProperty(XName.Get("defaultdocument", "DAV:"), "Properties|ContentService.DefaultDocument"),
			new ResourceProperty(XName.Get("iscollection", "DAV:"), "MimeType", o => o.ToString() == "application/x-folder" ? "1" : "0"),
			new ResourceProperty(XName.Get("Win32LastModifiedTime", "urn:schemas-microsoft-com:"), "LastModified", o => ((DateTime)o).ToUniversalTime().ToString("r", CultureInfo.InvariantCulture)),
			new ResourceProperty(XName.Get("Win32CreationTime", "urn:schemas-microsoft-com:"), "Properties|Win32CreationTime"),
			new ResourceProperty(XName.Get("Win32LastAccessTime", "urn:schemas-microsoft-com:"), "Properties|Win32LastAccessTime"),
		};

		/// <summary>
		/// WebDav Basic Item Properties
		/// </summary>
		/// <remarks>
		/// These properties are common to all items in the collection.
		/// </remarks>
		private static IEnumerable<IResourceProperty> ItemBasic = new IResourceProperty[] { 
			new ResourceProperty(XName.Get("id", MC_NAMESPACE)),
			new ResourceProperty(XName.Get("externalid", MC_NAMESPACE)),
			new ResourceProperty(XName.Get("parentid", MC_NAMESPACE)),
			new ResourceProperty(XName.Get("version", MC_NAMESPACE)),
			new ResourceProperty(XName.Get("name", MC_NAMESPACE)),
			new ResourceProperty(XName.Get("published", MC_NAMESPACE)),
			new ResourceProperty(XName.Get("mimetype", MC_NAMESPACE)),
			new ResourceProperty(XName.Get("resourceprovider", MC_NAMESPACE), "specification"),
			new ResourceProperty(XName.Get("relationships", MC_NAMESPACE), "relationships", s=> s.Key.StartsWith("RELATIONSHIP|", StringComparison.Ordinal) || s.Key.Equals("HREF"), o => FormatRelationship(o))
		};

		/// <summary>
		/// Explicit Properties are those that are not returned unless they are asked for.
		/// </summary>
		/// <remarks>Any expensive or other wise obscure type of property should be in this set.  The Version Control
		/// Spec RFC3253 states that all version control properties must not be returned unless asked for by name.
		/// The Supported Live Property Set property will return a listing of these properties.
		/// </remarks>
		private static IEnumerable<IResourceProperty> ExplicitProperties = new IResourceProperty[] {
			new ResourceProperty(XName.Get("href", MC_NAMESPACE),"href", o => (o as Uri).ToWebsiteUri()),
			new ResourceProperty(XName.Get("securityset", MC_NAMESPACE), "itemClaimId"),
			new ResourceProperty(XName.Get("permissions", MC_NAMESPACE), "permissions", s=> s.Key.StartsWith("ACCESSCLAIM|", StringComparison.Ordinal), o => FormatClaim(o)),
			new ResourceProperty(XName.Get("leftversion", MC_NAMESPACE)),
			new ResourceProperty(XName.Get("rightversion", MC_NAMESPACE)),
			new ResourceProperty(XName.Get("createdby", MC_NAMESPACE)),
			new ResourceProperty(XName.Get("version-date", MC_NAMESPACE), "LastModified", o=> ((DateTime)o).ToString("g", CultureInfo.InvariantCulture)),
			new ResourceProperty(XName.Get("version-name", "DAV:"), "version", o => String.Format(CultureInfo.InvariantCulture, "V{0}", o)),
			new ResourceProperty(XName.Get("root-version", "DAV:"), "id", s=> s.Key.Equals("HREF", StringComparison.Ordinal), o => ((Uri)o).ToWebsiteUri() + "?ver=1"),
			new ResourceProperty(XName.Get("comment", "DAV:"), "comment"),
			new ResourceProperty(XName.Get("creator-displayname", "DAV:"), "createdby"),
			new ResourceProperty(XName.Get("supported-method-set", "DAV:"), "Id", o=> from n in Enum.GetNames(typeof(Http.HttpMethods)) select new XElement(n)),
			new ResourceProperty(XName.Get("supported-live-property-set", "DAV:"), "Id", o=> from p in ExplicitProperties select p.Name),
			new ResourceProperty(XName.Get("supported-report-set", "DAV:")),
			new ResourceProperty(XName.Get("checked-in", "DAV:"), "Properties|checked-in", s=> s.Key.Equals("HREF", StringComparison.Ordinal), o=> new XElement(XName.Get("Href", "DAV:"), o)),
			new ResourceProperty(XName.Get("checked-out", "DAV:"), "Properties|checked-out",  s=> s.Key.Equals("HREF", StringComparison.Ordinal), o=> new XElement(XName.Get("Href", "DAV:"), o)),

			/*
			 * the predecessor property needs some help because the item href is request along with left and optionally right properties for its output.
			new ResourceProperty(XName.Get("predecessor-set", "DAV:"), "id", s=> s.Key.Equals("RIGHTVERSION", StringComparison.Ordinal) || s.Key.Equals("LEFTVERSION", StringComparison.Ordinal), o=> new XElement(XName.Get("Href", "DAV:"), "?ver=" + o.ToString())),
				# DAV:auto-version
				# DAV:successor-set
				# DAV:checkout-fork (in-place-checkout or working resource)
				# DAV:checkin-fork (in-place-checkout or working resource)
			 */
		};

		/// <summary>
		/// Formats the IItemRelationship as an XElement for output in the Relationships node.
		/// </summary>
		/// <param name="relation">The relation</param>
		/// <returns>A relation XElement</returns>
		private static IEnumerable<XElement> FormatRelationship(object relation)
		{
			IItemRelationship r = relation as IItemRelationship;
			if (r != null && r.TargetItem != null)
			{
				return new XElement[] {new XElement(XName.Get("relation", MC_NAMESPACE),
					new XAttribute("target", r.TargetItem.ToWebsiteUri()),
					new XAttribute("type", r.RelationshipType ?? "unknown"),
					new XAttribute("state", r.State ?? String.Empty)
					)}; //<mc:relation target=\"{0}\" type=\"{1}\" state=\"{2}\"/>
			}
			else
				return InboundRelationships(relation);
		}

		/// <summary>
		/// Formats the IItemClaim as an XElement for output in the Permissions node.
		/// </summary>
		/// <param name="claim">The claim.</param>
		/// <returns>an xml node for the claim</returns>
		private static XElement FormatClaim(object claim)
		{
			IItemClaim c = claim as IItemClaim;
			if (c != null)
			{
				return new XElement(XName.Get("claim", MC_NAMESPACE),
					new XAttribute("id", c.Id),
					new XAttribute("type", c.ClaimType),
					new XAttribute("value", c.ClaimValue),
					new XAttribute("rights", c.Rights.ToString())
					); //<mc:claim id=\"{0}\" type=\"{1}\"  value=\"{2}\" rights=\"{3}\"/>
			}
			return null;
		}

		/// <summary>
		/// Queries out all inbound relationships.  This is a listing of all items that reference the given item.
		/// </summary>
		/// <param name="o">An object boxed Uri parameter for the item to find references to</param>
		/// <returns>A listing of all item references</returns>
		private static IEnumerable<XElement> InboundRelationships(object o)
		{
			Uri itemUri = o as Uri;
			if (itemUri == null)
				return Enumerable.Empty<XElement>();

			List<XElement> relations = new List<XElement>();

			var standardLuceneAnalyzer = new Lucene.Net.Analysis.Standard.StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_29);
			Lucene.Net.Index.Term term = new Lucene.Net.Index.Term("Relationships", GetSanitizedTarget(itemUri.ToString()));
			Lucene.Net.Search.TermQuery query = new Lucene.Net.Search.TermQuery(term);
			var searcher = RequestHandlerFactory.CreateIndexSearcher();

			if (searcher != null)
			{
				var results = searcher.Search(query, null, 1000).scoreDocs;

				foreach (var item in results)
				{
					Lucene.Net.Documents.Document hit = searcher.Doc((item as Lucene.Net.Search.ScoreDoc).doc);
					relations.Add(new XElement(XName.Get("reference", MC_NAMESPACE), new XAttribute("source", new Uri(hit.Get("Href")).ToWebsiteUri()), new XAttribute("type", "Undefined")));
				}
			}
			return relations;
		}

		private static string GetSanitizedTarget(string target)
		{
			Regex regex = new Regex("[^a-zA-Z0-9]", RegexOptions.IgnoreCase);
			return regex.Replace(target, String.Empty).ToLower();
		}


		/// <summary>
		/// Parses the Item specification model and creates a listing of IResourceProperties that select DavData
		/// as appropriate and include attribute data for the item specification constraints. Also includes all
		/// of the non model specific properties that are valid on the given item.
		/// </summary>
		/// <param name="context">The context of the request</param>
		/// <param name="specId">The resource provider id.</param>
		/// <param name="itemUri">The item URI.</param>
		/// <returns>a listing of IResourceProperties for the Item Model Specification and global properties</returns>
		public static IEnumerable<IResourceProperty> ItemSpecification(DavContext context, Guid specId, Uri itemUri)
		{
			ResourcePropertyComparer resourcePropertyComparer = new ResourcePropertyComparer();

			IEnumerable<IResourceProperty> itemBasic = Properties.ItemBasic;
			IEnumerable<IResourceProperty> modelProperties = Properties.ModelProperties(context, specId, itemUri);

			return Properties.Basic.Concat(Properties.Extended.Concat(itemBasic.Where(e => !modelProperties.Contains(e, resourcePropertyComparer)).Concat(modelProperties)));
		}

		/// <summary>
		/// Parses the Item specification model and creates a listing of IResourceProperties that select DavData
		/// as appropriate and include attribute data for the item specification constraints. Also includes all
		/// of the non model specific properties that are valid on the given item.
		/// Also includes properties that must be explicitly requested to be returned on an item.  The properties
		/// are frequently those that require extra work or processing time to return.
		/// </summary>
		/// <param name="specId">The item specification id.</param>
		/// <param name="itemUri">The item URI.</param>
		/// <returns>a listing of IResourceProperties for the Item Model Specification and global properties</returns>
		public static IEnumerable<IResourceProperty> ItemSpecificationExplicit(DavContext context, Guid specId, Uri itemUri)
		{
			return Properties.ExplicitProperties.Concat(ItemSpecification(context, specId, itemUri));
		}

		/// <summary>
		/// Parses the Item specification model and creates a listing of IResourceProperties that select DavData
		/// as appropriate and include attribute data for the item specification constraints.
		/// </summary>
		/// <param name="specId">The resource provider id.</param>
		/// <param name="itemUri">The item URI.</param>
		/// <returns>a listing of IResourceProperties for the Item Model Specification</returns>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "we are using lowercase for xml tag names.")]
		public static IEnumerable<IResourceProperty> ModelProperties(DavContext context, Guid specId, Uri itemUri)
		{
			IItemSpecification itemModel = Handler.ItemSpecificationRequest(context.User, itemUri);

			if (itemModel == null)
				yield break;

			var itemNamespace = ItemNamespace(specId);

			var elements = itemModel.Properties
				.Select(e => new ModelProperty(
					new XElement(XName.Get(e.Name.ToLowerInvariant(), e.IsExtended ? itemNamespace : MC_NAMESPACE),
					ItemModelAttributes(e)),
					e.IsExtended ? "PROPERTIES|" + e.Name : e.Name
					));
			foreach (var e in elements)
				yield return e;
		}

		/// <summary>
		/// Returns XAttributes for each property of the given IModelProperty
		/// </summary>
		/// <param name="prop">The prop.</param>
		/// <returns>A list of XAttributes for the IModelProperty</returns>
		private static List<XAttribute> ItemModelAttributes(IModelProperty prop)
		{
			List<XAttribute> attributes = new List<XAttribute>();
			attributes.Add(new XAttribute(XName.Get("displayType"), prop.DisplayType));
			if (!string.IsNullOrEmpty(prop.ErrorMessage))
				attributes.Add(new XAttribute(XName.Get("errorMessage"), prop.ErrorMessage));
			if (!string.IsNullOrEmpty(prop.Label))
				attributes.Add(new XAttribute(XName.Get("label"), prop.Label));
			attributes.Add(new XAttribute(XName.Get("required"), prop.Required));
			attributes.Add(new XAttribute(XName.Get("userEditable"), prop.UserEditable));
			if (prop.Validation != null)
				attributes.Add(new XAttribute(XName.Get("validationExpression"), prop.Validation.ToString()));
			if(!string.IsNullOrEmpty(prop.ValidValues))
				attributes.Add(new XAttribute(XName.Get("validValues"), prop.ValidValues));
			// UNDONE: Store, Style and Vector are all indexing properties that should not be (?) added to the list.
			return attributes;
		}

		/// <summary>
		/// Builds the item property list.
		/// The Item property list is a complete listing of all properties that are defined on an item.
		/// </summary>
		/// <param name="specId">The item specification id.</param>
		/// <param name="itemUri">The item URI.</param>
		/// <returns>A list of all properties available on a given item.  Includes properties defined in the item specification model.</returns>
		private static IEnumerable<IResourceProperty> BuildItemPropertyList(DavContext context, Guid specId, Uri itemUri)
		{
			ResourcePropertyComparer resourcePropertyComparer = new ResourcePropertyComparer();
			string cacheKey = String.Format(CultureInfo.InvariantCulture, "ItemModel:{0}", specId);
			IEnumerable<IResourceProperty> response = (IEnumerable<IResourceProperty>)Cache[cacheKey];
			if (response == null)
			{
				IEnumerable<IResourceProperty> itemBasic = Properties.ItemBasic;
				IEnumerable<IResourceProperty> modelProperties = Properties.ModelProperties(context, specId, itemUri);

				response = Properties.Basic.Concat(Properties.Extended.Concat(itemBasic.Where(e => !modelProperties.Contains(e, resourcePropertyComparer)).Concat(modelProperties))).ToArray();
				Cache.Add(cacheKey, response, null, DateTime.MaxValue, TimeSpan.FromMinutes(10), CacheItemPriority.High, null);
			}
			return response;
		}

		#endregion

		/// <summary>
		/// Creates a PropInfo Web Dav response based on a given set of requested properties with a collection
		/// of DavData generated off of a set of items.
		/// </summary>
		/// <param name="dataSet">The data set.</param>
		/// <param name="requestedProperties">The requested properties.</param>
		/// <returns>An enumeration of PropInfo Response elements</returns>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "You can't have a generic dictionary without nesting generic types.")]
		public static IEnumerable<XElement> PropInfoResponse(this IEnumerable<IDictionary<string, object>> dataSet, DavContext context, IEnumerable<XName> requestedProperties)
		{
			foreach (var data in dataSet)
			{
				IEnumerable<IResourceProperty> propertyList = BuildItemPropertyList(context, (Guid)data["SPECIFICATION"], (Uri)data["HREF"]);
				if (requestedProperties != null && requestedProperties.Any())
				{
					propertyList = from p in propertyList.Concat(ExplicitProperties)
								   join r in requestedProperties on p.Name equals r
								   select p;
					// TODO: if propertyList.Count < requestedProperties.Count we have 404 properties.  these must go out in another propstat collection.
				}

				XStreamingElement okStatus = new XStreamingElement(XName.Get("propstat", "DAV:"));
				okStatus.Add(new XStreamingElement(XName.Get("prop", "DAV:"),
					from prop in propertyList select prop.Element(data.Where(prop.DataValueSelector).Select(p => p.Value))));
				okStatus.Add(new XElement(XName.Get("status", "DAV:"), "HTTP/1.1 200 OK"));

				yield return new XElement(XName.Get("response", "DAV:"),
					new XAttribute(XNamespace.Xmlns + "mcs", ItemNamespace((Guid)data["SPECIFICATION"])),
					new XElement(XName.Get("href", "DAV:"), FormatHref((Uri)data["HREF"])),
					okStatus);
			}
		}

		/// <summary>
		/// Formats the href with a query string as needed
		/// </summary>
		/// <param name="href">The href.</param>
		/// <returns>the absolute path of the href with the optional querystring</returns>
		private static string FormatHref(Uri href)
		{
			if (href == null)
				return string.Empty;
			else if (string.IsNullOrEmpty(href.Query))
				return href.AbsolutePath;
			else
				return String.Format(CultureInfo.InvariantCulture, "{0}{1}", href.AbsolutePath, href.Query);
		}

		/// <summary>
		/// Converts each item in the collection into a dictionary of Dav Data Properties.
		/// </summary>
		/// <param name="items">The items.</param>
		/// <param name="loadOptions">The load options indicating which data to include in the output set</param>
		/// <returns>A set of web dav raw data</returns>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "You can't have a generic dictionary without nesting generic types."),
		System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Dav", Justification = "DAV is a proper term in a web dav implementation")]
		public static IEnumerable<IDictionary<string, object>> DavData(this IEnumerable<IItemInstance> items, Model.LoadOptions loadOptions)
		{
			foreach (var item in items)
			{
				if (item == null)
					continue;

				var data = MapItemToData(item, loadOptions);
				yield return data;
			}
		}

		/// <summary>
		/// Converts a single item into a dictionary of Dav Data Properties.
		/// </summary>
		/// <param name="item">The item.</param>
		/// <param name="loadOptions">The load options.</param>
		/// <returns>A data dictionary for the item which is raw webdav property data</returns>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "You can't have a generic dictionary without nesting generic types."),
		System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Dav", Justification = "DAV is a proper term in a web dav implementation")]
		public static IEnumerable<IDictionary<string, object>> DavData(this IItemInstance item, Model.LoadOptions loadOptions)
		{
			var data = MapItemToData(item, loadOptions);
			yield return data;
		}

		/// <summary>
		/// Patches the href with version as a querystring parameter.
		/// </summary>
		/// <param name="davData">The dav data.</param>
		/// <returns>The original data data with the href modified to include the version explicitly</returns>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification="Creating a Generic List of Dictionaries requires this.")]
		public static IEnumerable<IDictionary<string, object>> PatchHrefWithVersion(this IEnumerable<IDictionary<string, object>> davData)
		{
			foreach (var data in davData)
			{
				int? version = (int?)data["VERSION"];
				Uri href = (Uri)data["HREF"];
				if (href != null && version.HasValue)
					data["HREF"] = new Uri(String.Format(CultureInfo.InvariantCulture, "{0}?ver={1}", href.OriginalString, version.Value));
				yield return data;
			}
		}

		/// <summary>
		/// Maps an item to a data dictionary
		/// </summary>
		/// <param name="item">The item which is the source of the data.</param>
		/// <param name="loadOptions">The load options for which data to load into the dictionary.</param>
		/// <returns>A data dictionary for the item.</returns>
		private static Dictionary<string, object> MapItemToData(IItemInstance item, Model.LoadOptions loadOptions)
		{
			if (!loadOptions.IsSet(Model.LoadOptions.Href))
				throw new ArgumentException("Href must be included in the item load specification in order to process items in webdav requests.", "loadOptions");

			Dictionary<string, object> properties = new Dictionary<string, object>();
			if (loadOptions.IsSet(Model.LoadOptions.Id))
				properties.Add("ID", item.Id);
			if (loadOptions.IsSet(Model.LoadOptions.ParentId))
				properties.Add("PARENTID", item.ParentId);
			if (loadOptions.IsSet(Model.LoadOptions.Version))
				properties.Add("VERSION", item.Version);
			if (loadOptions.IsSet(Model.LoadOptions.LeftVersion))
				properties.Add("LEFTVERSION", item.LeftVersion);
			if (loadOptions.IsSet(Model.LoadOptions.RightVersion))
				properties.Add("RIGHTVERSION", item.RightVersion);
			if (loadOptions.IsSet(Model.LoadOptions.CreatedBy))
				properties.Add("CREATEDBY", item.CreatedBy);
			if (loadOptions.IsSet(Model.LoadOptions.Comment))
				properties.Add("COMMENT", item.Comment);
			if (loadOptions.IsSet(Model.LoadOptions.ExternalId))
				properties.Add("EXTERNALID", item.ExternalId);
			if (loadOptions.IsSet(Model.LoadOptions.LastModified))
				properties.Add("LASTMODIFIED", item.LastModified);
			if (loadOptions.IsSet(Model.LoadOptions.MimeType))
				properties.Add("MIMETYPE", item.MimeType);
			if (loadOptions.IsSet(Model.LoadOptions.Name))
				properties.Add("NAME", item.Name);
			if (loadOptions.IsSet(Model.LoadOptions.Published))
				properties.Add("PUBLISHED", item.Published);
			if (loadOptions.IsSet(Model.LoadOptions.Specification))
				properties.Add("SPECIFICATION", item.Specification);
			if (loadOptions.IsSet(Model.LoadOptions.Href))
				properties.Add("HREF", item.Href);
		  if (loadOptions.IsSet(Model.LoadOptions.ItemClaimId))
			 properties.Add("SECURITYSET", item.ItemClaimId);	// will be renamed to security set from security hash

			int i = 0;
			if (loadOptions.IsSet(Model.LoadOptions.Relationships) && item.Relationships != null)
				foreach (var relation in item.Relationships)
					properties.Add("RELATIONSHIP|" + i++, relation);
			if (loadOptions.IsSet(Model.LoadOptions.Properties) && item.Properties != null)
				foreach (var prop in item.Properties)
					properties.Add("PROPERTIES|" + prop.KeyName.ToUpperInvariant(), prop.KeyValue);
			if (loadOptions.IsSet(Model.LoadOptions.AccessClaims) && item.AccessClaims != null)
				foreach (var claim in item.AccessClaims)
					properties.Add( string.Format(CultureInfo.CurrentCulture, "ACCESSCLAIM|{0}-{1}-{2}", claim.Id, claim.ClaimType, claim.ClaimValue).ToUpperInvariant(), claim);
			return properties;
		}

		/// <summary>
		/// Patches the property on the Item Property collection
		/// If the property value <paramref name="propValue"/> is null then the property is removed. Otherwise the
		/// property value is update or a new property added as appropriate.
		/// </summary>
		/// <param name="properties">The properties of the item</param>
		/// <param name="key">The key name of the property.</param>
		/// <param name="propValue">The value to set on the property.</param>
		/// <returns>a patched item property enumeration.</returns>
		public static IEnumerable<IItemProperty> PatchProperty(this IEnumerable<IItemProperty> properties, string key, string propValue)
		{
			bool foundProp = false;
			if (key.StartsWith("PROPERTIES|", StringComparison.OrdinalIgnoreCase))
				key = key.Substring(11);

			foreach (var prop in properties)
			{
				if (prop.KeyName.Equals(key, StringComparison.OrdinalIgnoreCase))
				{
					foundProp = true;
					if (string.IsNullOrEmpty(propValue))
						continue;
					else
						yield return new ItemProperty { KeyName = prop.KeyName, KeyValue = propValue };
				}
				else
					yield return prop;
			}
			// if we did not find our property and they are trying to set it then return it on the end of the collection.
			if (!foundProp && !string.IsNullOrEmpty(propValue))
				yield return new ItemProperty { KeyName = key, KeyValue = propValue };
		}

		/// <summary>
		/// Converts the Item Specification Id into an items namespace.
		/// </summary>
		/// <param name="providerId">The item Specification id.</param>
		/// <returns>A namespace uri for the given item specification</returns>
		private static string ItemNamespace(Guid itemSpecificationId)
		{
			return String.Format(CultureInfo.CurrentCulture, "uri://MetaCollection/v2/spec/{0}", itemSpecificationId);
		}

		/// <summary>
		/// A reference to our cache object.
		/// </summary>
		/// <remarks>TODO: When .NET 4.0 comes out with the System.Caching code we will want to rework this.</remarks>
		private static Cache Cache
		{
			get
			{
				return (System.Web.HttpContext.Current == null) ? System.Web.HttpRuntime.Cache : System.Web.HttpContext.Current.Cache;
			}
		}

		/// <summary>
		/// Gets a unique key for the given relationship.
		/// </summary>
		/// <param name="relation">The relation.</param>
		/// <returns>a key for the relationship</returns>
		public static string RelationKey(IItemRelationship relation)
		{
			return String.Format(CultureInfo.InvariantCulture, "{0}|{1}", relation.TargetItem.AbsolutePath, relation.RelationshipType).ToUpperInvariant();
		}

		/// <summary>
		/// Updates the relationships.
		/// </summary>
		/// <param name="relationships">The relationships.</param>
		/// <param name="relationProperty">The relationship property that contains the relations.</param>
		/// <returns>a patched set of relationships.</returns>
		public static IEnumerable<IItemRelationship> UpdateRelationships(IEnumerable<IItemRelationship> relationships, XElement relationProperty, bool isUpdate)
		{
			Dictionary<string, IItemRelationship> modifications = relationProperty.Elements(XName.Get("relation", MC_NAMESPACE)).ToDictionary(k => Properties.RelationKey(k.ToRelationship()), v => v.ToRelationship());
			HashSet<string> visitedKeys = new HashSet<string>();

			foreach (var relation in relationships)
			{
				string key = Properties.RelationKey(relation);
				if (modifications.Keys.Contains(key))
				{
					if (!isUpdate) // if this is not an update it is a remove.. we have a match so skip to the next.
						continue;
					else
						yield return modifications[key]; // an update, a match so on to the next.
					visitedKeys.Add(key);
				}
				else
					yield return relation; // not a modified realtion so return as normal.
			}
			if (isUpdate) // if we are updating then we need to return the rest of our updates that we did not see already go past. (not update then no need to remove something we did not see)
			{
				foreach (var key in modifications.Keys)
				{
					if (!visitedKeys.Contains(key))
						yield return modifications[key];
				}
			}
		}
		public static List<XName> RequestedProperties(XElement requestXml)
		{
			List<XName> requestedProperties = new List<XName>();

			if (requestXml != null)
			{
				var propNames = requestXml.Elements(XName.Get("prop", "DAV:")).FirstOrDefault();
				var includeName = requestXml.Elements(XName.Get("include", "DAV:")).FirstOrDefault();

				if (propNames != null)
					requestedProperties = requestedProperties.Concat(from n in propNames.Elements() where n.NodeType == XmlNodeType.Element select n.Name).ToList();

				if (includeName != null)
					requestedProperties = requestedProperties.Concat(from n in includeName.Elements() where n.NodeType == XmlNodeType.Element select n.Name).ToList();
			}
			return requestedProperties;
		}

		public static Model.LoadOptions LoadOptionsFromRequestedProperties(List<XName> requestedProperties)
		{
			if (requestedProperties == null || requestedProperties.Count == 0)
				return Model.LoadOptions.ItemComplete;

			// The load options that you can't get an item without.
			Model.LoadOptions loadOptions = Model.LoadOptions.Href | Model.LoadOptions.MimeType | Model.LoadOptions.Name | Model.LoadOptions.Specification;

			foreach (var prop in requestedProperties)
			{
				switch (prop.LocalName.ToLowerInvariant())
				{
					case "version":
						loadOptions = loadOptions | Model.LoadOptions.Version;
						continue;
					case "rightversion":
						loadOptions = loadOptions | Model.LoadOptions.RightVersion;
						continue;
					case "published":
						loadOptions = loadOptions | Model.LoadOptions.Published;
						continue;
					case "parentid":
						loadOptions = loadOptions | Model.LoadOptions.ParentId;
						continue;
					case "name":
						loadOptions = loadOptions | Model.LoadOptions.Name;
						continue;
					case "leftversion":
						loadOptions = loadOptions | Model.LoadOptions.LeftVersion;
						continue;
					case "id":
						loadOptions = loadOptions | Model.LoadOptions.Id;
						continue;
					case "externalid":
						loadOptions = loadOptions | Model.LoadOptions.ExternalId;
						continue;
					case "createdby":
						loadOptions = loadOptions | Model.LoadOptions.CreatedBy;
						continue;
					case "comment":
						loadOptions = loadOptions | Model.LoadOptions.Comment;
						continue;
					case "resourceprovider":
						loadOptions = loadOptions | Model.LoadOptions.Specification;
						continue;
					case "accessclaims":
						loadOptions = loadOptions | Model.LoadOptions.AccessClaims;
						continue;
					case "relationships":
						loadOptions = loadOptions | Model.LoadOptions.Relationships;
						continue;
					case "version-name":
						loadOptions = loadOptions | Model.LoadOptions.Version;
						continue;
					case "version-date":
						loadOptions = loadOptions | Model.LoadOptions.LastModified;
						continue;
					case "creator-displayname":
						loadOptions = loadOptions | Model.LoadOptions.CreatedBy;
						continue;
					case "predecessor-set":
						loadOptions = loadOptions | Model.LoadOptions.LeftVersion | Model.LoadOptions.RightVersion;
						continue;
					default:
						loadOptions = loadOptions | Model.LoadOptions.Properties;
						continue;
				}
			}
			return loadOptions;
		}
	}
	public static partial class Extensions
	{
		/// <summary>
		/// Root collection folder as defined in configuration file.
		/// </summary>
		private static string RootName = ((ContentConfigurationSection)ConfigurationManager.GetSection("contentConfiguration")).RootName;

		/// <summary>
		/// Replaces the scheme and authority of the uri with the current website's scheme and authority.  Also includes the application
		/// path (if present) so that when the module is not installed in the root the application will still function correctly.
		/// </summary>
		/// <param name="collectionUri">The collection URI.</param>
		/// <returns>a uri of the collection resource as presented on the website</returns>
		public static Uri ToWebsiteUri(this Uri collectionUri)
		{
			if (collectionUri != null)
			{
				if (string.IsNullOrEmpty(collectionUri.Query))
					return new Uri(String.Format(CultureInfo.InvariantCulture, "{0}{1}{2}{3}/{4}", HttpContext.Current.Request.Url.Scheme, Uri.SchemeDelimiter, HttpContext.Current.Request.Url.Authority, HttpContext.Current.Request.ApplicationPath.TrimEnd('/'), collectionUri.AbsolutePath.TrimStart('/')));
				else
					return new Uri(String.Format(CultureInfo.InvariantCulture, "{0}{1}{2}{3}/{4}?{5}", HttpContext.Current.Request.Url.Scheme, Uri.SchemeDelimiter, HttpContext.Current.Request.Url.Authority, HttpContext.Current.Request.ApplicationPath.TrimEnd('/'), collectionUri.AbsolutePath.TrimStart('/'), collectionUri.Query));
			}
			else
				return new Uri(String.Format(CultureInfo.InvariantCulture, "{0}{1}{2}", HttpContext.Current.Request.Url.Scheme, Uri.SchemeDelimiter, HttpContext.Current.Request.Url.Authority));
		}

		/// <summary>
		/// Replaces the scheme and authority of the uri with the current collection's scheme and root folder.  Also removes the application
		/// path (if present) so that when the module is not installed in the root the application will still function correctly.
		/// </summary>
		/// <param name="websiteUri">The website URI.</param>
		/// <returns>a collection uri</returns>
		public static Uri ToCollectionUri(this Uri websiteUri)
		{
			if (websiteUri == null)
				throw new  ArgumentNullException("websiteUri");

			return new Uri(String.Format(CultureInfo.InvariantCulture, "col://{0}/{1}", RootName, websiteUri.AbsolutePath.Substring(HttpContext.Current.Request.ApplicationPath.Length).TrimStart('/')));
		}

		/// <summary>
		/// Attempts to convert the XElement that represents a relation into an IItemRelationship
		/// <mc:relation target="" type="" state="" />
		/// </summary>
		/// <param name="relationElement">The relation element.</param>
		/// <returns>an IItemRelationship where the XElement is a relation otherwise null</returns>
		public static IItemRelationship ToRelationship(this XElement relationElement)
		{
			if (relationElement == null || !relationElement.Name.LocalName.Equals("relation", StringComparison.OrdinalIgnoreCase))
				return null;
			ItemRelationship relation = new ItemRelationship();
			relation.RelationshipType = (string)relationElement.Attributes("type").FirstOrDefault();
			relation.State = (string)relationElement.Attributes("state").FirstOrDefault();
			string target = (string)relationElement.Attributes("target").FirstOrDefault();
			if (!string.IsNullOrEmpty(target))
				relation.TargetItem = new Uri(target).ToCollectionUri();
			return relation;
		}
	}
}
